/**
 * 
 */
package com.googlecode.cswish.struts.spring;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorValue;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import net.sf.ehcache.CacheManager;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.TermQuery;
import org.apache.struts2.StrutsStatics;
import org.hibernate.MappingException;
import org.hibernate.Session;
import org.hibernate.cache.ehcache.internal.regions.EhcacheDataRegion;
import org.hibernate.exception.ConstraintViolationException;
import org.hibernate.hql.internal.ast.QuerySyntaxException;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.jdbc.ReturningWork;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.FullTextQuery;
import org.hibernate.search.jpa.Search;
import org.springframework.scheduling.annotation.Async;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.annotation.GenericType;
import com.googlecode.cswish.annotation.Protected;
import com.googlecode.cswish.annotation.Rest;
import com.googlecode.cswish.exception.ApplicationException;
import com.googlecode.cswish.model.ModelInfo;
import com.googlecode.cswish.model.PageElements;
import com.googlecode.cswish.model.PageInfo;
import com.googlecode.cswish.model.ParameterMap;
import com.googlecode.cswish.model.Reference;
import com.googlecode.cswish.oosearch.Condition;
import com.googlecode.cswish.oosearch.QLInfo;
import com.googlecode.cswish.oosearch.QLParse;
import com.googlecode.cswish.security.Context;
import com.googlecode.cswish.security.SafeManager;
import com.googlecode.cswish.security.auth.UserInfo;
import com.googlecode.cswish.security.type.RecordRuleSetOr;
import com.googlecode.cswish.struts.ContextHolder;
import com.googlecode.cswish.struts.conversion.PageElementService;
import com.googlecode.cswish.struts.hibernate.DynamicModelManager;
import com.googlecode.cswish.struts.hibernate.DynamicModelPersistent;
import com.googlecode.cswish.struts.hibernate.IDynamic;
import com.googlecode.cswish.struts.hibernate.IDynamicModel;
import com.googlecode.cswish.util.ClassUtil;
import com.googlecode.cswish.util.FrameConstant;
import com.googlecode.cswish.util.Functions;
import com.googlecode.cswish.util.OgnlExUtil;
import com.googlecode.cswish.util.bean.ModelUtil;
import com.googlecode.cswish.util.bean.ScannerContext;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.util.ValueStack;

/**
 * It provide the basic CRUD operations and OO search<br>
 * 
 * @author Jerry.Feng Date: 2008-3-30
 */
//@Service
//@Transactional
public abstract class GenericJPAService implements GenericService {
	
	private static final Log logger = LogFactory.getLog(GenericJPAService.class);
	
	@PersistenceContext(unitName="espirit")
	protected EntityManager em;
	
	@Resource
	protected QLParse qlParse;
		
	@Resource
	protected ObjectFactory objectFactory;
	
	@Resource
	protected BeanIntrospector beanIntrospector;

	@Resource
	protected OgnlExUtil ognlUtil;
	
	@Resource
	protected Config config;
	
	@Resource
	protected SafeManager safeManager;

	@Resource
	private Functions functions;
	
	@Resource
	private CacheService cacheService;
	
	private Map<String, List<ExecutorInfo>> events;
	
	@PostConstruct
	private void init() {
		functions.registCommonService("generic", this);
		events = cacheService.newCaches(ExecutorInfo.class);
	}
	
	private GenericJPAService genericJPAService;
	
	private CacheManager cacheManager;
	
	public GenericJPAService getThis() {
		if (genericJPAService == null) {
			genericJPAService = objectFactory.getBean(GenericJPAService.class, false);
		}
		return genericJPAService;
	}
	
	public CacheManager getCacheManager() {
		if (cacheManager == null) {
			Session session = (Session) em.getDelegate();
			Map cacheRegions = ((SessionFactoryImpl)session.getSessionFactory()).getAllSecondLevelCacheRegions();
			Object region = cacheRegions.values().iterator().next();
			cacheManager = ((EhcacheDataRegion)region).getEhcache().getCacheManager();
		}
		return cacheManager;
    }
	
	private class ExecutorInfo {
		
		public ExecutorInfo(Executor executor, boolean sync) {
			this.executor = executor;
			this.sync = sync;
		}
		
		private boolean sync;
		private Executor executor;
	}
	
	private PageElementService pageElementService;
	protected PageElementService getPageElementService() {
		if (pageElementService == null) {
			pageElementService = objectFactory.getBean(PageElementService.class, false);
		}
		return pageElementService;
	}
	
    public <T> PageInfo<T> search(T model, int start, int limit,
    		String sort, String dir, boolean newSearch, Reference<QLInfo> qlInfoRef) {
    	return this.search(model, start, limit, sort, dir, newSearch, qlInfoRef, null, true, false);
    }
    
    public SearchCallback createDefaultSearchCallback(final String modelName, final Object model, final String sort, final String dir) {
    	SearchCallback searchCallback = new SearchCallback() {
			@Override
			public QLInfo afterBuildQL(boolean newSearch, QLInfo qlInfo) {
				if (StringUtils.isEmpty(qlInfo.getSortAndDir())) {
					qlInfo.setDir(dir);
					qlParse.setSort(qlInfo, modelName, model, sort);
					ActionContext context = ActionContext.getContext();
					if (context != null) {
						// see QLInfo.update
						ValueStack vs = context.getValueStack();
						vs.setValue("sort", sort);
						vs.setValue("dir", dir);
					}
				}
				return qlInfo;
			}
    	};
    	return searchCallback;
    }
    
    @Protected
    public <T> PageInfo<T> search(T model, int start, int limit, String sort, String dir, boolean calculateCount) {
    	PageInfo<T> pageInfo = search(model, start, limit, sort, dir, true, null, null, calculateCount, false);
    	return pageInfo;
    }
    
    @Protected
    public <T> PageInfo<T> search(T model, int start, int limit, String sort, String dir,
    		boolean newSearch, Reference<QLInfo> qlInfoRef, SearchCallback callback, 
    		boolean calculateCount, boolean disablePermission) {
    	PageInfo<T> pageInfo;
    	if (model == null) {
    		pageInfo = new PageInfo<T>(config.getPageSize());
    		pageInfo.setData(new ArrayList<T>(0));
    	} else {
	    	QLInfo qlInfo = buildQlInfo(model, start, limit, sort, dir, newSearch, qlInfoRef, callback, disablePermission);
	    	
			if (callback != null) {
				
			}
	    	
			// caculate the count if there's not total count
			boolean isExactCount = false;
			int count;
			if ( calculateCount && qlInfo.getDefaultCount() == -1) {
				count = getCount(qlInfo, false);
				isExactCount = true;
			} else {
				count = qlInfo.getDefaultCount();
				// NOTICE: maybe the count is change when refreshing the page
				isExactCount = false;
			}
			qlInfo.setDefaultCount(count);
			
	    	if (!isExactCount || count > 0 || qlInfo.getFirstResult() < count) {
	    		pageInfo = searchByQl(qlInfo, false);
	    		pageInfo.setTotalCount(count);
	    	} else {
	    		pageInfo = new PageInfo<T>(config.getPageSize());
	    		pageInfo.setData(new ArrayList<T>(0));
	    	}
    	}
		return pageInfo;
    }

	protected <T> QLInfo buildQlInfo(T model, int start, int limit, String sort, String dir,
			boolean newSearch, Reference<QLInfo> qlInfoRef, SearchCallback callback, boolean disablePermission) {
		// get it from pageInfo
		QLInfo qlInfo = qlInfoRef == null ? null : qlInfoRef.get();
		if (newSearch || qlInfo == null) {
			if (model == null) {
				// try to initialize the model according the request action name
				model = (T) objectFactory.buildBean(GenericType.MODEL, Object.class, null);
			}
			
			// for the Map model, use the advanced search
			Condition searchCondition;
			if (model instanceof ParameterMap) {
				String complexSearch = ((ParameterMap)model).getParameter(ParameterMap.COMPLEX_SEARCH);
				if ("false".equals(complexSearch)) {
					searchCondition = Condition.SEARCH_LIKEFIRST;
				} else {
					// the default value is advance search
					searchCondition = Condition.ADVANCE_SEARCH;
				}
			} else {
				searchCondition = Condition.SEARCH_LIKEFIRST;
			}
			
			qlInfo = qlParse.getFromClause(model, searchCondition);
			if (qlInfoRef != null) {
				qlInfoRef.set(qlInfo);
			}
		}
		
		PageElements ps = null;
		if (newSearch && StringUtils.isEmpty(sort)) {
			ps = getPageElementService().getCurrentPageElements(false);
    	}
		// TODO: build new Condition & move this logic to condition?(except for default sort & write back the sort)
		qlInfo.update(start, limit, config.getPageSize(), sort, dir, qlParse, model, newSearch, ps);
		qlInfo.setDisablePermissionFilter(disablePermission);
		
		if (callback != null) {
			qlInfo = callback.afterBuildQL(newSearch, qlInfo);
			if (logger.isErrorEnabled()) {
				if (qlInfo == null) {
					logger.error("QLInfo is null after running callback function: " + callback.getClass().getName());
				}
			}
		}
		return qlInfo;
	}
    
    /**
     * Convert model to QL expression and return the table result
     * 
     * @param model
     * @param condition
     * @return
     */
    @Protected
    public <T> PageInfo<T> searchByModel(T model, Condition condition) {
    	return searchByModel(model, condition, false, false);
    }
    
    @Protected
    public <T> PageInfo<T> searchByModel(T model, Condition condition, boolean calculateTotalCount, boolean checkPermission) {
    	PageInfo<T> pageInfo = null;
    	if (model != null) {
	    	QLInfo qlInfo = qlParse.getFromClause(model, condition);
	    	if (!checkPermission) {
	    		qlInfo.setDisablePermissionFilter(true);
	    	}
	    	pageInfo = searchByQl(qlParse.toSelectHQL(qlInfo), false);
	    	
	    	if (calculateTotalCount) {
		    	int count = getCount(qlInfo, false);
		    	
		    	if (count > 0 && condition.getOffset() < count) {
		    		pageInfo.setTotalCount(count);
		    	}
	    	}
    	} else {
    		logger.debug("parameter 'model' is null");
    	}
    	
    	if (pageInfo == null) {
    		pageInfo = new PageInfo<T>(config.getPageSize());
    		pageInfo.setData(new ArrayList<T>(0));
    	}
    	return pageInfo;
    }
    
    protected abstract UserInfo getLoginUserInfo(String userId);
    
    @Protected
    public void activePermissionFilter(QLInfo qlInfo, String userId) {
    	if (qlInfo.isWithPermissionFilter() || !safeManager.isEnablePermissionCheck()) {
    		return;
    	}
    	ActionContext actionContext =  ActionContext.getContext();
    	if (actionContext == null) {
    		return ;
    	}
    	
    	String modelPath = null;
    	Context context = new Context(actionContext.getValueStack(), functions);
    	// get current user id
    	if (userId == null) {
	    	HttpServletRequest request = (HttpServletRequest) actionContext.get(StrutsStatics.HTTP_REQUEST);
	    	UserInfo userInfo = safeManager.getLoginUser(request);
	    	if (userInfo == null) {
	    		return ;
	    	}
	    	userId = userInfo.getLoginId();
	    	context.setUserInfo(userInfo);
    	} else {
    		// ensure the user permission is loaded
//    		modelPath = getModelName(qlInfo.getQl());
//    		int sitePosBegin = FrameConstant.PACKAGE_PREFIX.length();
//    		int sitePosEnd = modelPath.indexOf('.', FrameConstant.PACKAGE_PREFIX.length() + 1);
//    		String site = modelPath.substring(sitePosBegin, sitePosEnd);
    		String site = ContextHolder.get().getSiteName();
    		safeManager.login((HttpSession)null, userId, site);
    		UserInfo userInfo = getLoginUserInfo(userId);
    		context.setUserInfo(userInfo);
    	}
    	
    	if (modelPath == null) {
    		modelPath = getModelName(qlInfo.getQl());
    	}
    	String path = functions.linkModelNoBase(modelPath, "search", true);
    	RecordRuleSetOr recordRuleSetOr = safeManager.getRecordLimitPermission(userId, path, context);
    	String oldQl = qlInfo.getQl();
    	if (recordRuleSetOr != null) {
			String filterCondition = recordRuleSetOr.getWhereClause(context);
		
    		if (!"true".equals(filterCondition)) {
	    		int whereBegin = oldQl.indexOf(" where ");
	    		boolean hasWhereCondition = (whereBegin != -1);
	    		
	    		String fromCondition = recordRuleSetOr.getFromClause();
	    		if (fromCondition != null) {
	    			if (hasWhereCondition) {
	    				oldQl = oldQl.substring(0, whereBegin) + fromCondition + oldQl.substring(whereBegin);
	    			} else {
	    				oldQl = oldQl + fromCondition;
	    			}
	    		}
	    		
	    		String ql;
	    		int end = oldQl.lastIndexOf(" group by ");
	    		if (end == -1) {
	    			end = oldQl.lastIndexOf(" order by ");
	    		}
	    		if (end == -1) {
	    			ql = oldQl + (hasWhereCondition ? " and " : " where ") + '(' + filterCondition + ')';
	    		} else {
	    			ql = oldQl.substring(0, end) + (hasWhereCondition ? " and " : " where ") + '(' + filterCondition + ')' + oldQl.substring(end);
	    		}
	    		qlInfo.setQl(ql);
    		}
    	}
    	qlInfo.setDisablePermissionFilter(true);
    }

	private String getModelName(String oldQl) {
		// from modelName a where
    	int modelBegin = oldQl.indexOf("from ") + 5;
    	if (modelBegin < 5) {
    		// wrong QL?
    		modelBegin = 0;
    	}
    	int modelEnd = oldQl.indexOf(' ', modelBegin + 1);
    	if (modelEnd == -1) {
    		modelEnd = oldQl.length();
    	}
    	String modelPath = oldQl.substring(modelBegin, modelEnd);
    	if (modelPath.indexOf('.') == -1) {
    		String fullName = beanIntrospector.guessFullClassName(modelPath.toLowerCase());
    		if (fullName != null) {
    			modelPath = fullName;
    		}
    	}
		return modelPath;
	}
    
    @Protected
    public <T> PageInfo<T> searchFulltext(T model, Condition condition) {
    	PageInfo<T> pageInfo = null;
    	
    	if (model != null) {
	    	QLInfo qlInfo = qlParse.getFromClause(model, condition);
	    	
	    	FullTextEntityManager fullTextEm = Search.getFullTextEntityManager(em);
	    	BooleanQuery bq = new BooleanQuery();
	
	    	int index = 0;
	        for (String paramName : qlInfo.getParamNames()) {
	           TermQuery tq = new TermQuery(new Term(paramName, qlInfo.getParams().get(index++).toString()));
	           bq.add(new BooleanClause(tq, BooleanClause.Occur.MUST));
	        }
	
	        FullTextQuery query = fullTextEm.createFullTextQuery(bq, model.getClass());
	        
	    	pageInfo = new PageInfo(config.getPageSize());
	    	if (qlInfo.getMaxResults() != 0) {
				// pager query
				query.setFirstResult(qlInfo.getFirstResult());
				query.setMaxResults(qlInfo.getMaxResults());
	
				List<T> result = query.getResultList();
				int size = qlInfo.getMaxResults();
				pageInfo.setPageSize(size);
				pageInfo.setPageNoByRowNo(qlInfo.getFirstResult());
				pageInfo.setData(result);
			} else {
				// query all data
				List<T> result = query.getResultList();
				int size = result == null? 0: result.size();
				pageInfo.setPageSize(size);
				pageInfo.setTotalCount(size);
				pageInfo.setData(result);
			}
    	} else {
    		logger.debug("parameter 'model' is null");
    	}
    	
    	if (pageInfo == null) {
    		pageInfo = new PageInfo<T>(config.getPageSize());
    		pageInfo.setData(new ArrayList<T>(0));
    	}
    	return pageInfo;
    }
    
    /**
     * search all the data result
     * 
     * @param model
     * @return
     */
    @Protected
    public <T> PageInfo<T> searchAll(@GenericType T model) {
    	if (model == null) {
			// try to initialize the model according the request action name
			model = (T) objectFactory.buildBean(GenericType.MODEL, Object.class, null);
		}
    	QLInfo qlInfo = qlParse.getFromClause(model, Condition.DEFAULT);
    	// TODO: control the max result for performance reason
    	qlInfo.setMaxResults(1000);
    	// TODO: use lazy collection to improve the performance
    	QLInfo selectQlInfo = qlParse.toSelectHQL(qlInfo);
    	selectQlInfo.setDisablePermissionFilter(true);
    	PageInfo<T> pageInfo = searchByQl(selectQlInfo, false);
    	return pageInfo;
    }
    
    /**
     * calculate the record count
     * 
     * @param qlInfo
     * @return
     */
    @Protected
    public int getCount(final QLInfo qlInfo, final boolean cacheable) {
    	activePermissionFilter(qlInfo, null);
    	
    	String ql = qlInfo.getQl();
    	String countQl;
    	if (StringUtils.startsWithIgnoreCase(ql, "select count(")) {
    		countQl = ql;
    	} else {
	    	int begin = 0;
	    	if (ql.startsWith("select a ")) {
	    		begin = 9;
	    	}
			int end = ql.lastIndexOf(" order by ");
			if (end == -1) {
				end = ql.length();
			}
			if (begin != 0 || end > 0) {
				countQl = "select count(a) " + ql.substring(begin, end);
			} else {
				countQl = "select count(a) " + ql;
			}
    	}
		
    	final String queryQl = countQl;
		int count = (Integer)runHibernateApi(ql, new Executor() {
			@Override
			public Object execute(Object parameters) {
				Query query = em.createQuery(queryQl);
				if (cacheable) {
					query.setHint("org.hibernate.cacheable", true);
				}
				if (qlInfo.getParams() != null) {
					int index = 1;
		    		for (Object value : qlInfo.getParams()) {
						query.setParameter(index++, value);
					}
		    	}
				
				Object value = query.getSingleResult();
				return Integer.parseInt(value.toString());
			}
		});
		
		return count;
    }
    
    @Protected
    public PageInfo searchByQl(QLInfo qlInfo) {
    	return searchByQl(qlInfo, false);
    }
    
    /**
     * search table data
     * 
     * @param qlInfo
     * @return
     */
    @Protected
    public PageInfo searchByQl(final QLInfo qlInfo, final boolean cacheable) {
    	activePermissionFilter(qlInfo, null);
    	
    	String ql = qlInfo.getQlWithSort();
    	
    	final String qlWithSelect;
    	if (!ql.startsWith("select ")) {
    		qlWithSelect = "select a " + ql;
    	} else {
    		qlWithSelect = ql;
    	}
    	
    	PageInfo pageInfo = (PageInfo)runHibernateApi(qlWithSelect, new Executor() {
			@Override
			public Object execute(Object parameters) {
				Query query = em.createQuery(qlWithSelect);
		    	if (cacheable) {
		    		query.setHint("org.hibernate.cacheable", true); 
		    	}
		    	if (qlInfo.getParams() != null) {
		    		int index = 1;
		    		for (Object value : qlInfo.getParams()) {
						query.setParameter(index++, value);
					}
		    	}
		    	
		    	PageInfo pageInfo = new PageInfo(config.getPageSize());
		    	if (qlInfo.getMaxResults() != 0) {
					// pager query
		    		if (qlInfo.getFirstResult() >= 0) {
		    			query.setFirstResult(qlInfo.getFirstResult());
		    		}
		    		if (qlInfo.getMaxResults() >= 0) {
		    			query.setMaxResults(qlInfo.getMaxResults());
		    		}

					List<?> result = query.getResultList();
					int size = qlInfo.getMaxResults();
					pageInfo.setPageSize(size);
					pageInfo.setPageNoByRowNo(qlInfo.getFirstResult());
					pageInfo.setTotalCount(result.size());	// Don't get real the total count
					pageInfo.setData(result);
				} else {
					// query all data
					List<?> result = query.getResultList();
					int size = result == null ? 0: result.size();
					pageInfo.setPageSize(size);
					pageInfo.setPageNoByRowNo(qlInfo.getFirstResult());
					pageInfo.setTotalCount(size);
					pageInfo.setData(result);
				}
		    	return pageInfo;
			}
		});
    	
    	boolean isArrayResult = !pageInfo.getData().isEmpty() &&pageInfo.getData(0)!= null && pageInfo.getData(0).getClass().isArray();
    	if (isArrayResult) {
    		String[] columnInfos = null;
    		
    		List<Map> newData = new ArrayList<Map>();
    		for (Object item : pageInfo.getData()) {
    			
    		}
    	}
    	return pageInfo;
    }
    
    @Transactional
    public <T> List<T> addList(List<T> model) {
    	List<T> result = new ArrayList<T>(model.size());
    	for (T theModel : model) {
    		result.add(add(theModel));
    	}
    	return result;
    }
    
    public void _add(@GenericType Object model) {
    }
    
	@Transactional
    @Rest(type="_add")
    public <T> T add(@GenericType T model) {
    	if (load(model) != null) {
    		throw new ApplicationException("errors.duplicate", getId(model));
    	}
    	
    	if (model != null) {
    		// update the reference
    		ModelUtil.updateParentChildRelationshipAndDbModel(model, null);
    		model = merge(model);
    	}
    	return model;
    }
    
	@Transactional
    @Protected
    public <T> T merge(final T model) {
    	return mergeWithEvent(model, null);
    }
    
	@SuppressWarnings("unchecked")
    private <T> T mergeWithEvent(final T model, final Object oldDbModel) {
        // update
    	final String className = ClassUtil.getClassName(model);
    	T newModel = (T)runHibernateApi(className, new Executor() {
			@Override
			public Object execute(Object parameters) {
				List<ExecutorInfo> executorInfos = events.get(className + '.' + OPERATION_MERGE);
				Object theOldDbModel = null;
    			if (executorInfos != null) {
    				if (oldDbModel == null) {
    					Object id = getId(model);
    					if (id != null) {
		    				Object theModel = em.find(ClassUtil.getClass(model), id);
		    				theOldDbModel = ModelUtil.newInstance(ClassUtil.getClass(theModel));
		    				ModelUtil.copyIgnoreNullValue(theModel, theOldDbModel);
    					}
    				} else {
    					theOldDbModel = oldDbModel;
    				}
    			}
    			T newModel;
    			ModelInfo modelInfo = beanIntrospector.getModelInfo(ClassUtil.getClassName(model));
    			if (modelInfo.isDynamicClass() && !(model instanceof IDynamic)) {
    				T dModel = (T) ModelUtil.newInstance(modelInfo.getTypeClass());
    				ModelUtil.copy(model, dModel);
    				newModel = em.merge(dModel);
    			} else {
    				newModel = em.merge(model);
    			}
	    		em.flush();
	    		if (executorInfos != null) {
	    			triggerEvent(executorInfos, newModel, theOldDbModel, OPERATION_MERGE);
    			}
	    		return newModel;
			}
		});
    	return newModel;
    }
    
	private void triggerEvent(List<ExecutorInfo> executorInfos,
			Object newModel, Object oldDbModel, String operation) {
		for (ExecutorInfo executorInfo : executorInfos) {
			Executor executor = executorInfo.executor;
			Object param = new Object[] {newModel, oldDbModel, operation};
			if (executorInfo.sync) {	// not async
				executor.execute(param);
			} else {
				getThis().executeAsync(executor, param);
			}
		}
	}
	
	protected Object getId(Object model) {
		return ModelUtil.getSimpleProperty(model, "id");
    }
    
	@Override
	@Protected
	@Async
	public <T, P> T executeAsync(Executor<T, P> executor, P parameters) {
		try {
			return executor.execute(parameters);
		} catch (Exception ex) {
			logger.error("Run asyn method", ex);
			throw ex;
		}
	}
	
	@Override
	@Protected
	@Transactional
	public <T, P> T executeInSite(String site, Executor<T, P> executor, P parameters) {
		ContextHolder contextHolder = ContextHolder.get();
		if (site == null) {			//  || site.equals(contextHolder.getSiteName()) ==> check current connection
			return executor.execute(parameters);
		}
		
		String oldSite = contextHolder.getSiteName();
		try {
			contextHolder.setSiteName(site);
			return getThis().executeInNewTx(executor, parameters);
		} finally {
			contextHolder.setSiteName(oldSite);
		}
	}
	
	@Protected
	@Transactional
//	@Transactional(propagation=Propagation.REQUIRES_NEW)
	public <T, P> T executeInNewTx(Executor<T, P> executor, P parameters) {
		return executor.execute(parameters);
	}
	
    public void updateList(List<Object> model) {
    	if (model != null) {
	    	for (Object theModel : model) {
	    		update(theModel);
	    	}
    	}
    }
    
    @Transactional
    @Rest(type="_update")
    public Object update(@GenericType Object model) {
        return update(model, false, getEnableFields(false, false), null);
    }
    
    protected Set<String> getEnableFields(boolean includeTextModelField, boolean useExactPath) {
    	PageElements ps = getPageElementService().getCurrentPageElements(true);
    	Set<String> enableFields = ps.getEnableFields(includeTextModelField, useExactPath, null);
    	return enableFields == null ? new HashSet(0) : enableFields;
    }
    
    @Protected
    @Transactional
    public <T> T update(T model, boolean disablePermissionCheck, 
    		Collection<String> enableFields, Executor<ScannerContext, ScannerContext> entityInterceptor) {
    	Object id = getId(model);
    	if (id == null) {
    		throw new ApplicationException("error.missId");
    	}
    	
    	// it's not necessary to use ClassUtil.getClass to ensure the real detail class
    	T existed = (T)load(model.getClass(), id);
    	if (existed == null) {
    		throw new ApplicationException("error.missModel");
    	}
    	
    	Object oldDbModel;
    	if (model != existed) {
    		if (!disablePermissionCheck) {
    			// check the record permission
    			if (!checkRecordPermission(existed, "update")) {
    				throw new ApplicationException("errors.recordPermission");
    			}
    		}
    		
    		List<ExecutorInfo> executorInfos = events.get(ClassUtil.getClassName(model) + '.' + OPERATION_MERGE);
			if (executorInfos != null) {
				oldDbModel = ModelUtil.newInstance(ClassUtil.getClass(existed));
				ModelUtil.copyIgnoreNullValue(existed, oldDbModel);
			} else {
				oldDbModel = null;
			}
    		
			// update the changed fields
    		// FIXME: at present, don't check field permission because the enableFields doesn't populate the back layer change,
			// add use security filter to check the input parameters?
    		// ModelUtil.deepUpdate(model, existed, enableFields, enableFields);
			ModelUtil.updateParentChildRelationship(model, null);
    		ModelUtil.deepUpdate(model, existed, entityInterceptor, null, enableFields); 
    	} else {
    		oldDbModel = existed;
    	}
    	
        // update
    	existed = mergeWithEvent(existed, oldDbModel);
    	return existed;
    }
    
    // TODO: check the operation level automatically? view --> update/delete, which means user must have the 
    // view permission before updating and deleting
    private boolean checkRecordPermission(Object model, String method) {
    	if (model == null) {
    		return true;
    	}
    	ActionContext actionContext =  ActionContext.getContext();
    	if (actionContext == null) {
    		return true;
    	}
    	HttpServletRequest request = (HttpServletRequest) actionContext.get(StrutsStatics.HTTP_REQUEST);
    	UserInfo userInfo = safeManager.getLoginUser(request);
    	if (userInfo == null) {
    		return true;
    	}
    	
    	String modelName = ClassUtil.getClass(model).getName();
    	String path = functions.linkModelNoBase(modelName, method, true);
    	Context context = new Context(actionContext.getValueStack(), functions);
    	context.setModel(model);
    	context.setUserInfo(userInfo);
    	RecordRuleSetOr recordRuleSetOr = safeManager.getRecordLimitPermission(userInfo.getLoginId(), path, context);
    	if (recordRuleSetOr == null || RecordRuleSetOr.FALSE.equals(recordRuleSetOr)) {
    		return true;
    	} else {
    		return recordRuleSetOr.executeBoolean(context);
    	}
    }
    
    @Transactional
    public boolean deleteList(List<Object> results) {
    	boolean ret = true;
    	if (results != null) {
			for (Object theModel : results) {
				if (theModel != null) {
					ret &= delete(theModel);
				}
			}
    	}
    	return ret;
    }

    @Transactional
    public boolean delete(@GenericType Object model) {
    	return delete(model, false);
    }
    
    @Transactional
    @Protected
    public boolean delete(@GenericType Object model, boolean disablePermissionCheck) {
    	// TODO: check the model id?
    	if (model != null) {
    		final Object dbModel = load(model);
    		if (dbModel != null) {
    			if (dbModel != model && !disablePermissionCheck) {
					if (!checkRecordPermission(dbModel, "deleteList")) {
						throw new ApplicationException("errors.recordPermission");
	    			}
    			}
    			
    			final String className = ClassUtil.getClassName(model);
    			runHibernateApi(className, new Executor() {
    				@Override
    				public Object execute(Object parameters) {
    					em.remove(dbModel);
    	    			em.flush();
    	    			List<ExecutorInfo> executorInfos = events.get(className + '.' + OPERATION_DELETE);
    	    			if (executorInfos != null) {
    	    				triggerEvent(executorInfos, null, dbModel, OPERATION_DELETE);
    	    			}
    	    			return null;
    				}
    			});
    			return true;
    		}
    	}
    	return false;
    }
    
    protected Object runHibernateApi(String ql, Executor execute) {
    	if (!config.isEnableSaas()) {
    		return execute.execute(null);
    	} else {
    		try{
        		return execute.execute(null);
    		} catch (ConstraintViolationException ex){
    			logger.debug("Failed to call hibernate api", ex);			// status == 23000
    			throw new ApplicationException("errors.reference");
    		} catch (MappingException | PersistenceException ex) {
    			String detailMsg = ex.getMessage();
    			if (detailMsg.indexOf("could not execute statement") >= 0) {
    				logger.error("Failed to call hibernate api", ex);
    				throw ex;
    			} else {
    				return addHibernateDefinition(ql, execute, ex);
    			}
    		} catch (IllegalArgumentException ex) {
    			Throwable cause = ex.getCause();
    			if (cause instanceof MappingException | cause instanceof QuerySyntaxException) {
    				return addHibernateDefinition(ql, execute, (RuntimeException)cause);
    			} else {
    				logger.error("Failed to call hibernate api", ex);
    				throw ex;
    			}
    		} catch (Exception ex) {
    			logger.error("Failed to call hibernate api", ex);
    			throw ex;
    		}
    	}
    }

	private Object addHibernateDefinition(String ql, Executor execute, RuntimeException ex) {
		int index = ql.indexOf(" from ");
		String entityName;
		if (index >= 0) {
			index += 6;
			int end = ql.indexOf(" ", index);
			if (end == -1) {
				end = ql.length();
			}
			entityName = ql.substring(index, end);
		} else {
			entityName = ql;
		}
		
		if (!beanIntrospector.isHibernateModel(entityName)) {
			try {
				DynamicModelManager dynamicModelManager = objectFactory.getBean(DynamicModelManager.class, false);
				List<Future<DynamicModelPersistent>> results = dynamicModelManager.addHibernateDefinition(entityName);
				for (Future<DynamicModelPersistent> result : results) {		// wait all task is done
					result.get();
				}
				// run again
				return execute.execute(null);
			} catch (Exception e) {
				logger.error("Add dynamic hibernate definition failed", e);
				throw new ApplicationException(e.getMessage());
			}
		} else {
			throw ex;
		}
	}
    
    public Object _update(@GenericType Object model) {
    	return view(model);
    }
    
    /**
     * load the model object and update the result to input parameter 
     */
    public <T> T view(@GenericType T model) {
    	Object dbModel = load(model, false);
    	if (model instanceof Map) {
    		((Map)model).putAll((Map)dbModel);
    	} else {
    		if (dbModel != null) {    			
    			ModelUtil.copy(dbModel, model);
    		}
    	}
    	return model;
    }
    
    @SuppressWarnings("unchecked")
	@Protected
    public <T> T load(T model) {
    	return load(model, true);
    }
    
    @SuppressWarnings("unchecked")
	@Protected
    public <T> T load(T model, boolean disablePermissionCheck) {
    	final Object id = getId(model);
    	T ret;
    	if (id != null && id instanceof Serializable ) {
            if (model instanceof Map) {	// JPA doesn't support the dynamic model
            	Object theName = ((Map)model).get(IDynamicModel.ENTITY_NAME);
            	final String className;
    			if (theName instanceof String) {
    				className = (String) theName;
    			} else {
    				className = null;
    			}
            	
            	ret = (T)runHibernateApi(className, new Executor() {
					
					@Override
					public Object execute(Object parameters) {
						Query query = em.createQuery(" from " + className + " where id=?");
		          		query.setParameter(1, id);
		                return (T)query.getSingleResult();
					}
				});
            } else {
            	ret = (T)load(model.getClass(), id);
            }
            
            if (!disablePermissionCheck) {
        		if (!checkRecordPermission(ret, "view")) {
        			throw new ApplicationException("errors.recordPermission");
        		}
        	}
    	} else {
    		ret = null;
    	}
    	return ret;
    }
    
    @Protected
    public <T> List<T> load(Class<T> clazz, final List<Object> ids, boolean disablePermissionCheck) {
    	List<T> results = new ArrayList<T>(ids.size());
    	for (Object id : ids) {
    		T model = load(clazz, id);
    		if (model != null) {
    			results.add(model);
    		}
    	}
    	
    	if (!disablePermissionCheck && results.size() > 0) {
    		ActionContext actionContext =  ActionContext.getContext();
        	if (actionContext != null) {
	        	HttpServletRequest request = (HttpServletRequest) actionContext.get(StrutsStatics.HTTP_REQUEST);
	        	UserInfo userInfo = safeManager.getLoginUser(request);
	        	if (userInfo != null) {
	            	String modelName = ClassUtil.getClass(clazz.getName()).getName();
	            	String path = functions.linkModelNoBase(modelName, "search", true);
	            	Context context = new Context(actionContext.getValueStack(), functions);
	            	context.setUserInfo(userInfo);
	            	RecordRuleSetOr recordRuleSetOr = safeManager.getRecordLimitPermission(userInfo.getLoginId(), path, context);
	            	
	            	if (recordRuleSetOr != null && !RecordRuleSetOr.FALSE.equals(recordRuleSetOr)) {
	            		List<T> resultsWithPermission = new ArrayList<T>(results.size());
	            		
	            		for (T model : results) {
		            		context.setModel(model);
		            		if (recordRuleSetOr.executeBoolean(context)) {
		            			resultsWithPermission.add(model);
		            		}
	            		}
	            		
	            		results = resultsWithPermission;
	            	}
	        	}
        	}
    	}
    	return results;
    }
    
	@Protected
    public <T> T load(Class<T> clazz, final Object id) {
		final Class loadClass;
		if (ModelInfo.isDynamicClass(clazz.getName()) 			// TODO: use interface
				|| HibernateProxy.class.isAssignableFrom(clazz)) {
			loadClass = clazz.getSuperclass();
		} else {
			loadClass = clazz;
		}
		
		@SuppressWarnings("unchecked")
		T dbModel = (T)runHibernateApi(loadClass.getName(), new Executor() {
			@Override
			public Object execute(Object parameters) {
				return em.find(loadClass, id);
			}
		});
		return dbModel;
    }
	
	@Protected
	@Transactional
	public <T> T doReturningWork(ReturningWork<T> work) {
		Session session = (Session)em.getDelegate();
		return session.doReturningWork(work);
	}
	
	@Override
    public <T> T changeModelType(Object model, Class<T> targetType) {
    	Session session = (Session)em.getDelegate();
        session.evict(model);
                
        Class originalType = model.getClass();
        DiscriminatorColumn discriminatorColumn = (DiscriminatorColumn)originalType.getAnnotation(DiscriminatorColumn.class);
        while (discriminatorColumn == null && (originalType = originalType.getSuperclass()) != null) {
        	discriminatorColumn = (DiscriminatorColumn)originalType.getAnnotation(DiscriminatorColumn.class);
        }
        if (discriminatorColumn == null) {
        	logger.warn("Invalid model type: " + model.getClass());
        	return null;
        }
        String newType = null;
        DiscriminatorValue discriminatorValue = targetType.getAnnotation(DiscriminatorValue.class);
        if (discriminatorValue != null) {
        	newType = discriminatorValue.value();
        } else {
        	newType = targetType.getSimpleName();
        }
        
        String ql = "update " + originalType.getName() + " " +
                "set " + discriminatorColumn.name() + " = :newType " +
                "where id = :id)";
        org.hibernate.Query query = session.createQuery(ql);
        Serializable id = (Serializable) getId(model);
        query.setParameter("id", id);
        
        
        query.setString("newType", newType);
        query.executeUpdate();
        session.flush();
        T newModel = (T) session.get(originalType, id);
        return newModel;
    }
	
	@Protected
	public synchronized void on(Class<?> clazz, String operation, Executor<?, Object[]> execute, boolean sync) {
		String[] operations = StringUtils.split(operation, ',');
		for (String opt : operations) {
			List<ExecutorInfo> infos = null;
			String key = clazz.getName() + '.' + opt;
			infos = events.get(key);
			if (infos == null) {
				infos = new ArrayList<ExecutorInfo>();
				events.put(key, infos);
			}
			
			if (infos != null) {
				infos.add(new ExecutorInfo(execute, sync));
			}
		}
    }
	
    // for com.googlecode.cswish.struts.BaseStrutsTestCaseTest
    public void test(Object model) {}
}